folditfandomcom-20200222-history
Lua packaging for Foldit
Foldit recipes written in Lua can be quite large. Recipes like Tvdl enhanced DRW 3.1.1 and TvdL DRemixW 3.1.2 are around 2,000 lines of code. (TvdL is Timo van der Laan.) Dealing with such large chunks of code can be problematic. It can be hard to find the line you're looking for, and it's easy to miss problems. Another issue is that recipes often contain general code that can be used in other contexts. For example, the two TvdL recipes referenced above contain Lua functions that select puzzle segments in various ways. Many other recipes can use these functions. The usual way to limit complexity and allow reuse is to break a large program into smaller pieces. Unfortunately, the Lua environment provided by Foldit doesn't provide the usual methods. In the cookbook, a Foldit recipe must be complete, with all the code it needs. At runtime, there's no way for a recipe to call another recipe or include code from another recipe. The functions in the Foldit Lua Interface are packaged Lua-style as a set of tables. The tables have names like structure, band, dialog, and selection. The tables group the functions into categories. A recipe can actually modify the Foldit Lua interface by adding new functions or overriding existing functions, but there's no way to permanently extend or change the interface. The recipe must apply all its changes each time it runs. Despite the limitations, it's possible organize Foldit recipe code into psuedo-packages consisting of one or more Lua tables. Recipes still need to include the lines of code in a package, but each package can be a single block of code. Lua isn't an object-oriented language, but grouping functions and data fields into a table can provide some of the benefits of the classes found in other languages. There are least two different stylistic approaches to this type of packaging. The first is the "all in" style, placing all variables and functions inside the braces of a table definition. One advantage of this approach is that results in a single block of code. Many editors allow a block of code to be collapsed into a single line, making it easier to navigate large recipes. The "all in" does impose some limitations, however, as discussed below. The other style can be called "a la carte". It's similar to extending existing packages, like the structure, band, and dialog tables found in the Foldit Lua interface. In the "a la carte" method, the initial table definition can be empty, and then variables and functions can be added as needed. This method bypasses the restrictions of the "all in" style, but no longer allows the entire package to be collapsed and expanded easily in a text editor. Instead, the package consists of multiple blocks of code. It's also possible to combine both the "all in" and "a la carte" styles. A package could include a core set of variables and functions in the initial definition. Additional entries can be added separately, as needed. In either style or packaging, a function can be redefined as a "method", making it more similar to methods in a language with classes. The examples below contrast the two packaging styles, and discuss extending them with methods. "All in" packaging example The example below shows a table that contains both variables and functions. This is an example of the "all in" style, which has some special syntax requirements which may be unfamiliar. There's also a limitation on how variables are initialized. TPT = { var1 = 2, var2 = 2, Add = function () return TPT.var1 + TPT.var2 end, } The table is named TPT. The table contains two variables, named var1 and var2, and one function, named Add. The names used can be anything that matches Lua's naming rules (There's nothing magic about the style shown in the example, so you're free to use upper and lower case names as you see fit.) Syntax In the "all in" style, everything is added within the opening brace of the table definition. This means each variable and function definition must be terminated by a comma. (The comma can be omitted on the last table entry, but then of course, you need to remember to add a comma if you add a new last entry.) The function Add is must be declared using the functionname = function ( argumentlist ) style. The function functionname ( argumentlist ) version is more commonly used. The two styles of function definition are normally interchangeable, but inside a table defintion, the function's name must come first. Within the function Add, the two variables are referenced as TPT.var1 and TPT.var2. Likewise, the Add function must be called as TPT.Add. After their initial definition, any of the variables and functions defined as TPT table entries must be referenced using the "TPT." prefix, regardless of whether the reference occurs in one of the functions in the table or an outside function. Scope In Lua, all variables and functions are global unless they are specifically defined as local. Global variables and functions can be accessed anywhere in the program (or recipe), and persist as long as the program is running. Local variables can be accessed only in the function or block of code in which they are defined, and disappear when the function or block of code ends. This distinction between global and local is referred to as scope. In this example, assuming the table TPT is at global scope, the table entries TPT.var1, TPT.var2, and TPT.Add are also global. Their scope is not limited to the table TPT. If TPT is defined as local, then its variables and functions have the same scope. Unlike classes in other languages, there's no way to define variables that can only be accessed from functions in a particular table. So anyone can access and modify the variables TPT.var1 and TPT.var2. In really complex environments, this would be undesirable. A good class should keep its business private. In the Foldit environment, this is not a big issue, since all the source code for TPT is part of the recipe anyway, meaning there's no way to prevent tinkering. Limitations One final wrinkle is that the variable definitions in a table can't use other variable definitions as initializers with the "all in style". So adding this code after var2: var3 = TPT.var1 + TPT.var2, -- WRONG! won't work, since the name TPT is not defined until the TPT table definition closes. The error produced would be something like: ERROR: "TPT = {...":6: attempt to index global 'TPT' (a nil value) The initialization could be handled in code: var3 = 0, -- to be determined Init = function () TPT.var3 = TPT.var1 + TPT.var2 ) end, The function TPT.init() would then be called from outside the TPT table definition. This limitation only applies to variables and functions that are defined in TPT. Otherwise, initializers can use any functions and variables that are available. Functions defined inside the table can reference TPT names, as TPT.Add shows. This is possible because the references aren't evaluated until the function is actually called. The limitation on using TPT names really applies only to the initializers for variables. Usage The final part is actually calling the function Add: TPT = { var1 = 2, var2 = 2, Add = function () return TPT.var1 + TPT.var2 end, } result = TPT.Add () print ( "the result is " .. result ) the result is 4 Adding methods to "All in" One problem with the previous example is the table name "TPT" appearing throughout the code. It's a little clunky, and it means that all the functions are specific to just one table. This becomes a problem, for example, if you attempt to reuse the Add function in a different table: TPT2 = { var1 = 4, var2 = 6, } TPT2.Add = TPT.Add -- reuse TPT's Add result = TPT2.Add () print ( "the TPT2 result is " .. result ) the TPT2 result is 4 The function TPT2.Add still has references to TPT, so it adds TPT.var1 and TPT.var2 instead of their TPT2 counterparts. Lua provides a different type of function definition, called a "method". With the all-in style, a method is just a function defined with an extra first parameter, usually called "self" or "this". The function uses this first parameter to refer to functions and variables in the table. With Add defined as a method, the TPT table looks like this: TPT = { var1 = 2, var2 = 2, Add = function ( self ) return self.var1 + self.var2 end, } To call the method Add, use the colon operator: TPT:Add () This is the same as: TPT.Add ( TPT ) The method TPT.Add can now be reused: TPT2 = { var1 = 4, var2 = 6, } TPT2.Add = TPT.Add -- reuse TPT's Add result = TPT2:Add () print ( "the TPT2 result is " .. result ) the TPT2 result is 10 "A la carte" packaging example The example below shows the "a la carte" style for the package in the previous example. This style starts with defining an empty table. Variables and functions can then be added to the table. TPT = {} TPT.var1 = 2 TPT.var2 = 2 function TPT.Add () return TPT.var1 + TPT.var2 end Syntax In the "a la carte" style, all the syntax should be familiar. There are no commas after the variable and function definitions. All function and variable definitions must include the table name qualifier ("TPT.") from the start. Function definitions can use either the fucnction functionname ( argumentlist ) style or the functionname = function ( argumentlist ) style seen in the "all in" example. The "a la carte" style also allows use of methods, as discussed in the previous example. A method can be defined using the colon operator. This style has the effect of adding the variable "self" as the first parameter. The previous example can be rewritten to make Add () a method: TPT = {} TPT.var1 = 2 TPT.var2 = 2 function TPT:Add () return self.var1 + self.var2 end The Add method would be called as: TPT:Add () Scope The scoping considerations are the same as the "all in" style. The variables and functions in TPT have the same scope as the TPT itself. In the most common scenario, TPT would be defined as a global, so everything inside TPT would be global as well. Limitations Using the "a la carte" style, since the table TPT is defined at the start, it exists (is not nil) in the subsequent statements. Assuming that the variables TPT.var1 and TPT.var2 have also been defined, there's no problem with saying: TPT.var3 = TPT.var1 + TPT.var2 In the "all in" style, the table name "TPT" is nil until the closing brace, so this type of initialization won't work. Usage From user's point of view, there's no difference between the "all in" and "a la carte" styles. All that's needed is to call TPT.Add, either as a function or a method. If TPT.Add is defined as a standard function, the call is: result = TPT.Add () print ( "the result is " .. result ) the result is 4 If TPT.Add is defined as a method, the call is: result = TPT:Add () print ( "the result is " .. result ) the result is 4 Category:Recipes Category:Script tutorial